﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using System.Net;
using System.Text.RegularExpressions;
using System.Runtime.InteropServices;

namespace TrafficSplitter
{
    class DynDNS
    {
        [DllImport("kernel32")]
        private static extern long WritePrivateProfileString(string section,
            string key, string val, string filePath);

        [DllImport("kernel32")]
        private static extern int GetPrivateProfileString(string section,
            string key, string def, StringBuilder retVal,
            int size, string filePath);

        private string filename;
        private VirtualInterface localOnlyInterface;
        private bool enabled;
        private string username;
        private string password;
        private string hostnames;
        private bool srcTrafficSplitter;
        private string currentIP;
        private string previousIP;
        private long lastUpdated;
        //private bool changed;
        WebClient webclient;
        Timer timer;
        
        // Controls
        private CheckBox chkEnabled;
        private Button btnUpdate;
        private RadioButton rbTrafficSplitter;
        private RadioButton rbWhatsmyip;
        private TextBox tbUsername;
        private TextBox tbPassword;
        private TextBox tbHostnames;
        private Label lblLastUpdated;
        private Label lblPreviousIP;
        private Label lblChanged;
        private List<Control> lstControls;

        private string userAgent;

        private Dictionary<string, string> returnCodes;

        public DynDNS(String filename, VirtualInterface localInterface, 
            CheckBox enabled, Button update, Label previousIP, Label changed,
            RadioButton trafficSplitter, RadioButton whatsmyip, TextBox username,
            TextBox password, TextBox hostnames, Label lastUpdated)
        {
            this.filename = filename;
            this.localOnlyInterface = localInterface;
            this.chkEnabled = enabled;
            this.btnUpdate = update;
            this.rbTrafficSplitter = trafficSplitter;
            this.rbWhatsmyip = whatsmyip;
            this.tbUsername = username;
            this.tbPassword = password;
            this.tbHostnames = hostnames;
            this.lblLastUpdated = lastUpdated;
            this.lblPreviousIP = previousIP;
            this.lblChanged = changed;
            //this.changed = false;
            this.timer = new Timer();
            this.timer.Interval = 900000;   // 15 minutes
            this.timer.Enabled = false;
            this.timer.Tick += new EventHandler(TimerTicked);
            
            this.lstControls = new List<Control>();
            lstControls.Add(btnUpdate);
            lstControls.Add(rbTrafficSplitter);
            lstControls.Add(rbWhatsmyip);
            lstControls.Add(tbUsername);
            lstControls.Add(tbPassword);
            lstControls.Add(tbHostnames);
            lstControls.Add(lblLastUpdated);
            lstControls.Add(lblPreviousIP);
            lstControls.Add(lblChanged);

            this.returnCodes = new Dictionary<string, string>();
            // Account-Related errors
            returnCodes.Add("badauth", "The username and password pair do not match a real user.");
            returnCodes.Add("!donator", "An option available only to credited users (such as offline URL) was specified, but the user is not a credited user. If multiple hosts were specified, only a single !donator will be returned.");
            // Update complete
            returnCodes.Add("good", "The update was successful, and the hostname is now updated.");
            returnCodes.Add("nochg", "The update changed no settings, and is considered abusive. Additional nochg updates will cause the hostname to become blocked.");
            // Hostname-Related errors
            returnCodes.Add("notfqdn", "The hostname specified is not a fully-qualified domain name (not in the form hostname.dyndns.org or domain.com).");
            returnCodes.Add("nohost", "The hostname specified does not exist in this user account (or is not in the service specified in the system parameter)");
            returnCodes.Add("numhost", "Too many hosts (more than 20) specified in an update. Also returned if trying to update a round robin (which is not allowed)");
            returnCodes.Add("abuse", "The hostname specified is blocked for update abuse.");
            // Agent-Related errors
            returnCodes.Add("badagent", "The user agent was not sent or HTTP method is not permitted (we recommend use of GET request method).");
            returnCodes.Add("good 127.0.0.1", "This answer indicates good update only when 127.0.0.1 address is requested by update. In all other cases it warns user that request was ignored because of agent that does not follow our specifications.");
            // Server Error conditions
            returnCodes.Add("dnserr", "DNS error encountered");
            returnCodes.Add("911", "There is a problem or scheduled maintenance on our side.");

            this.webclient = new WebClient();
            webclient.CachePolicy = new System.Net.Cache.RequestCachePolicy(System.Net.Cache.RequestCacheLevel.NoCacheNoStore);
            userAgent = String.Format("{0}/{1} ({2})", Application.ProductName, Application.ProductVersion, Environment.OSVersion);

        }

        private void LoadFile()
        {
            username = LoadString("DDNS", "Username", "", 25);
            password = LoadString("DDNS", "Password", "", 25);
            hostnames = LoadString("DDNS", "Hostnames", "", 1001);
            srcTrafficSplitter = LoadBoolean("DDNS", "Source", "true");
            enabled = LoadBoolean("DDNS", "Enabled", "false");
            previousIP = LoadString("DDNS", "PreviousIP", "0.0.0.0", 16);
            try
            {
                lastUpdated = Convert.ToInt64(LoadString("DDNS", "LastUpdated", "0", 65));
                
            }
            catch (Exception)
            {
                lastUpdated = 0;
            }
            UpdateInfo();

            tbUsername.Text = username;
            tbPassword.Text = password;
            tbHostnames.Text = hostnames;
            rbTrafficSplitter.Checked = srcTrafficSplitter;
            chkEnabled.Checked = enabled;
        }

        public void InputChanged(object sender, EventArgs e)
        {
            if (sender == rbTrafficSplitter)
            {
                srcTrafficSplitter = rbTrafficSplitter.Checked;
                timer.Enabled = !srcTrafficSplitter & enabled;
            }

            //this.changed = true;
            this.lblChanged.Visible = true;
        }

        private string LoadString(String key, String name, String defaultValue, int maxLen)
        {
            StringBuilder temp = new StringBuilder(maxLen);
            GetPrivateProfileString(key, name, defaultValue, temp, temp.Capacity, this.filename);
            return temp.ToString();
        }

        private Boolean LoadBoolean(String key, String name, String defaultValue)
        {
            StringBuilder temp = new StringBuilder(10);
            GetPrivateProfileString(key, name, defaultValue, temp, temp.Capacity, this.filename);
            return Convert.ToBoolean(temp.ToString());
        }

        public void LoadSettings()
        {
            LoadFile();
            
            UpdateControls();
        }

        private void SaveFile()
        {
            WritePrivateProfileString("DDNS", "Username", username, this.filename);
            WritePrivateProfileString("DDNS", "Password", password, this.filename);
            WritePrivateProfileString("DDNS", "Hostnames", hostnames, this.filename);
            WritePrivateProfileString("DDNS", "Source", srcTrafficSplitter.ToString(), this.filename);
            WritePrivateProfileString("DDNS", "Enabled", enabled.ToString(), this.filename);
        }

        public void SaveSettings()
        {
            username = tbUsername.Text;
            password = tbPassword.Text;
            hostnames = tbHostnames.Text;
            srcTrafficSplitter = rbTrafficSplitter.Checked;

            if (!ValidateInputs(false))
                return;

            SaveFile();

            //changed = false;
            this.lblChanged.Visible = false;
            this.btnUpdate.Enabled = IsActive();
        }

        private void TimerTicked(object sender, EventArgs e)
        {
            Update(true);
        }

        public void Update()
        {
            Update(false);
        }

        private void CorrectTimer()
        {
            btnUpdate.Enabled = false;
            timer.Enabled = !srcTrafficSplitter & enabled;
            btnUpdate.Enabled = IsActive();
        }

        public void Update(bool silent)
        {
            timer.Stop();   // same as Enabled=false


            if (!ValidateInputs(silent))
            {
                CorrectTimer();
                return;
            }

            if (!ValidateMinimumInterval(silent))
            {
                CorrectTimer();
                return;
            }

            currentIP = GetIP();
            if (!ValidateIP(silent))
            {
                CorrectTimer();
                return;
            }

            string url = ConstructURL();
            //MessageBox.Show(String.Format("URL = '{0}'", url));

            Application.DoEvents();
            
            string results = "";
            try
            {
                // Source: http://www.kapie.com/blog/2007/06/19/CClassToUpdateDynDNS.aspx
                webclient.Credentials = new NetworkCredential(username, password);
                webclient.Headers.Add("User-Agent", userAgent);
                Constants.AppendToLog(LOG_TYPE.Debug, string.Format("DDNS : Requesting {0} with username {1}", url, username));
                results = webclient.DownloadString(url);

                Constants.AppendToLog(LOG_TYPE.Debug, "DDNS : Results received '" + results + "'");
                int pos = results.IndexOf(' ');
                if (pos > 0)
                    results = results.Substring(0, pos);
                
                if (returnCodes.ContainsKey(results))
                {
                    if (!silent)
                        MessageBox.Show(returnCodes[results], Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Information);
                }
                else
                {  
                    if (!silent)
                        MessageBox.Show("Invalid return code", Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
            }
            catch (WebException ex)
            {
                Constants.AppendToLog (LOG_TYPE.Error, "DDNS : " + ex.Message);
                if (ex.Status == WebExceptionStatus.ProtocolError)
                {
                    MessageBox.Show(ex.Message, Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error);
                }

                CorrectTimer();
                return;
            }

            // Update success ?
            if (results.Equals("nochg") || results.Equals("good"))
            {
                // Save new IP address
                previousIP = currentIP;
                WritePrivateProfileString("DDNS", "PreviousIP", previousIP, this.filename);

                // Save last updated time (in 1/100 000 of a second)
                lastUpdated = DateTime.Now.Ticks;
                UpdateInfo();
                WritePrivateProfileString("DDNS", "LastUpdated", lastUpdated.ToString(), this.filename);

                Constants.AppendToLog(LOG_TYPE.Info, string.Format("DDNS : Updated to {0} succeeded at {1}", lblPreviousIP.Text, lblLastUpdated.Text));
            }
            else if (results.Equals("notfqdn") || results.Equals("abuse") ||
                results.Equals("nohost") || results.Equals("badagent") || results.Equals("badauth") || results.Equals("badsys"))
            {
                // Critical error : Disable DynDNS
                chkEnabled.Checked = false;
                UpdateControls();
                SaveSettings();

                Constants.AppendToLog(LOG_TYPE.Error, "DDNS : Update failed");
            }

            CorrectTimer();
        }

        private void UpdateInfo()
        {
            if (lastUpdated == 0)
            {
                lblLastUpdated.Text = "Never";
            }
            else
            {
                DateTime dt = new DateTime(lastUpdated);
                lblLastUpdated.Text = dt.ToString("yyyy/MM/dd HH:mm:ss");
            }

            if ((previousIP == null) || (previousIP.Equals("0.0.0.0"))) {
                lblPreviousIP.Text = "0.0.0.0";
            } else {
                lblPreviousIP.Text = previousIP;
            }
        }

        private bool ValidateMinimumInterval(bool silent)
        {
            // Ensure that the DynDNS doesn't get updated more often than 15 minutes
            TimeSpan ts = new TimeSpan(DateTime.Now.Ticks - lastUpdated);
            double difference = ts.TotalMinutes;
            bool ret = difference > 10.00;
            if (!ret)
            {
                string msg = "Minimum update interval of 10 minutes not done yet";
                Constants.AppendToLog(LOG_TYPE.Warning, "DDNS : " + msg);
                if (!silent)
                {
                    DialogResult res = MessageBox.Show(msg + ".\n\nAre you sure you still want to force the update?", Application.ProductName, MessageBoxButtons.YesNo, MessageBoxIcon.Error, MessageBoxDefaultButton.Button2);
                    if (res == DialogResult.No)
                        return false;
                }
            }

            return ret;
        }

        private bool ValidateInputs(bool silent)
        {
            // Check for empty input fields
            if ((username == "") || (password == "") || (hostnames == ""))
            {
                string msg = "Invalid values specified for username/password/hostname(s)";
                Constants.AppendToLog(LOG_TYPE.Warning, "DDNS : " + msg);
                if (!silent)
                    MessageBox.Show(msg, Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error);

                return false;
            }

            return true;
        }

        private bool ValidateIP(bool silent)
        {
            string msg;

            // Check IP
            if (currentIP == null)
            {
                msg = "IP address unknown";
                Constants.AppendToLog(LOG_TYPE.Warning, "DDNS : " + msg);
                if (!silent) 
                    MessageBox.Show(msg, Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error);
                return false;
            }

            // Check IP remained the same
            if (currentIP == previousIP)
            {
                msg = "IP address did not change";
                Constants.AppendToLog(LOG_TYPE.Warning, "DDNS : " + msg);
                if (!silent)
                {
                    DialogResult res = MessageBox.Show(msg + ".\n\nAre you sure you still want to force the update?", Application.ProductName, MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2);
                    if (res == DialogResult.No)
                        return false;
                }
                else
                    return false;
            }

            // Check IP Format
            Match ret = Constants.ipPattern.Match(currentIP);
            if (!ret.Success)
            {
                msg = "IP address format is invalid";
                Constants.AppendToLog(LOG_TYPE.Warning, "DDNS : " + msg);
                if (!silent)
                    MessageBox.Show(msg, Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error);
                return false;
            }

            // Check for LAN IP addresses
            ulong ip = Routing.IP2Int(currentIP);
            if ((ip == 0) ||                            // 0.0.0.0
                ((ip & 0xFF000000) == 0x0A000000) ||    // 10.x.x.x
                ((ip & 0xFF000000) == 0x7F000000) ||    // 127.x.x.x
                ((ip & 0xFFFF0000) == 0xA9FE0000) ||    // 169.254.x.x
                ((ip & 0xFFF00000) == 0xAC100000) ||    // 172.16-31.x.x
                ((ip & 0xFFFF0000) == 0xC0A80000)) 	    // 192.168.x.x
            {
                msg = "LAN IP addresses are not allowed";
                Constants.AppendToLog(LOG_TYPE.Warning, "DDNS : " + msg);
                if (!silent)
                    MessageBox.Show(msg, Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error);
                ForceWhatsmyip();
                return false;
            }

            return true;
        }

        private void ForceWhatsmyip() {
            rbWhatsmyip.Checked = true;
            SaveSettings();
        }

        private string ConstructURL()
        {
            return String.Format(@"http://members.dyndns.org/nic/update?hostname={0}&myip={1}", hostnames, currentIP);
        }

        public void UpdateControls()
        {
            enabled = chkEnabled.Checked;

            foreach (Control ctrl in lstControls)
            {
                ctrl.Enabled = enabled;
            }

            timer.Enabled = !srcTrafficSplitter & enabled;

            //changed = false;
            this.lblChanged.Visible = false;
        }

        private string GetIP()
        {
            if (srcTrafficSplitter)
            {
                return localOnlyInterface.GetAdapter().GetIPAddress();
            }
            else
            {
                try
                {
                    return webclient.DownloadString(Constants.GET_LOCAL_IP);
                }
                catch (WebException ex)
                {
                    Constants.AppendToLog(LOG_TYPE.Error, "DDNS : " + ex.Message);
                    return null;
                }
            }
        }

        public bool IsActive()
        {
            return enabled;
        }
    }
}
